// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package conf
// is a component that defines the log configuration.
//
// Component is initialized from follow file when load at first time. Config
// file name should be "log.yaml" or "log.yml" at config directory like
// follows.
//
// One of follows:
//
//   - config/log.yaml
//   - config/log.yml
package conf

import (
	"gitee.com/go-wares/log/base"
	"gopkg.in/yaml.v3"
	"net"
	"os"
	"strings"
	"sync"
	"time"
)

const (
	DefaultTimeFormat              = time.RFC3339
	DefaultServiceName             = "logger"
	DefaultServiceVersion          = "0.0.0"
	DefaultOpentracingTraceId      = "X-B3-TraceId"
	DefaultOpentracingSpanId       = "X-B3-SpanId"
	DefaultOpentracingSpanPosition = "X-B3-Version"
)

var (
	once      = new(sync.Once)
	singleton *Config
)

type (
	// Config
	// is a component that defines the log configuration.
	Config struct {
		// Level
		// logging filter name like follows.
		//
		//    - debug
		//    - info (default)
		//    - warn
		//    - error
		//    - fatal
		Level base.Level `yaml:"level"`

		// TimeFormat
		// logger time format.
		TimeFormat string `yaml:"time-format"`

		// +-------------------------------------------------------------------+
		// | Opentracing Compatible Options                                    |                                                    |
		// +-------------------------------------------------------------------+

		// OpentracingTraceId
		// trace id for opentracing.
		OpentracingTraceId string `yaml:"opentracing-trace-id"`

		// OpentracingSpanId
		// span id for opentracing.
		OpentracingSpanId string `yaml:"opentracing-span-id"`

		// OpentracingSpanPosition
		// Package tracing for items offset position.
		OpentracingSpanPosition string `yaml:"opentracing-span-position"`

		// +-------------------------------------------------------------------+
		// | Logger Adapter Options		                                       |                                                    |
		// +-------------------------------------------------------------------+

		// Adapter
		// configured for logging like follows.
		//
		//   - elastic
		//   - file
		//   - kafka
		//   - term (default)
		Adapter base.Adapter `yaml:"adapter"`

		// ElasticAdapter
		// is a component that defines the Elasticsearch adapter configuration.
		ElasticAdapter *ElasticAdapterConfig `yaml:"elastic-adapter"`

		// FileAdapter
		// is a component that defines the file adapter configuration.
		FileAdapter *FileAdapterConfig `yaml:"file-adapter"`

		// KafkaAdapter
		// is a component that defines the Kafka adapter configuration.
		KafkaAdapter *KafkaAdapterConfig `yaml:"kafka-adapter"`

		// TermAdapter
		// is a component that defines the terminal adapter configuration.
		TermAdapter *TermAdapterConfig `yaml:"term-adapter"`

		// +-------------------------------------------------------------------+
		// | Tracer Adapter Options		                                       |                                                    |
		// +-------------------------------------------------------------------+

		// Tracer
		// is a tracer for follows.
		//
		//   - jaeger
		//   - zipkin
		Tracer base.Tracer `yaml:"tracer"`

		// JaegerTracer
		// is a component that defines the Jaeger configuration.
		JaegerTracer *JaegerTracerConfig `yaml:"jaeger-tracer"`

		// ZipkinTracer
		// is a component that defines the Zipkin configuration.
		ZipkinTracer *ZipkinTracerConfig `yaml:"zipkin-tracer"`

		// +-------------------------------------------------------------------+
		// | Execution Options	    	                                       |                                                    |
		// +-------------------------------------------------------------------+

		Pid            int    `yaml:"-"`
		ServiceAddr    string `yaml:"-"`
		ServicePort    int    `yaml:"-"`
		ServiceName    string `yaml:"-"`
		ServiceVersion string `yaml:"-"`

		// +-------------------------------------------------------------------+
		// | Status switch   	    	                                       |                                                    |
		// +-------------------------------------------------------------------+

		debugOn, infoOn, warnOn, errorOn, fatalOn bool
		manager                                   any
	}

	ConfigAdapterConfigured interface {
		// ConfigLoggerAdapter
		// configures the logger adapter.
		ConfigLoggerAdapter(adapter base.Adapter)
	}

	ConfigReporterConfigured interface {
		// ConfigTracerReporter
		// configures the tracer reporter.
		ConfigTracerReporter(adapter base.Tracer)
	}
)

// Get
// returns the singleton configuration instance.
func Get() *Config {
	return singleton
}

// New
// creates a new configuration instance with manager.
func New(m any) *Config {
	once.Do(func() { singleton = (&Config{manager: m}).scan().scanService().init() })
	return singleton
}

// +---------------------------------------------------------------------------+
// | Status switches                                                           |
// +---------------------------------------------------------------------------+

func (o *Config) DebugOn() bool { return o.debugOn }
func (o *Config) ErrorOn() bool { return o.errorOn }
func (o *Config) FatalOn() bool { return o.fatalOn }
func (o *Config) InfoOn() bool  { return o.infoOn }
func (o *Config) WarnOn() bool  { return o.warnOn }

// SetAdapter
// sets the log adapter.
func (o *Config) SetAdapter(s string) *Config {
	o.Adapter = base.Adapter(s).Generator()

	if v, ok := o.manager.(ConfigAdapterConfigured); ok {
		v.ConfigLoggerAdapter(o.Adapter)
	}
	return o
}

// SetLevel
// sets the log level.
func (o *Config) SetLevel(s string) *Config {
	o.Level = base.Level(s).Generator()

	switch o.Level {
	case base.LevelDebug:
		// [level=debug]
		o.debugOn = true
		o.infoOn = true
		o.warnOn = true
		o.errorOn = true
		o.fatalOn = true
	case base.LevelInfo:
		// [level=info]
		o.debugOn = false
		o.infoOn = true
		o.warnOn = true
		o.errorOn = true
		o.fatalOn = true
	case base.LevelWarn:
		// [level=warn]
		o.debugOn = false
		o.infoOn = false
		o.warnOn = true
		o.errorOn = true
		o.fatalOn = true
	case base.LevelError:
		// [level=error]
		o.debugOn = false
		o.infoOn = false
		o.warnOn = false
		o.errorOn = true
		o.fatalOn = true
	case base.LevelFatal:
		// [level=fatal]
		o.debugOn = false
		o.infoOn = false
		o.warnOn = false
		o.errorOn = false
		o.fatalOn = true
	default:
		o.debugOn = false
		o.infoOn = false
		o.warnOn = false
		o.errorOn = false
		o.fatalOn = false
	}
	return o
}

// SetTracer
// sets the tracer.
func (o *Config) SetTracer(s string) *Config {
	o.Tracer = base.Tracer(s).Generator()

	if v, ok := o.manager.(ConfigReporterConfigured); ok {
		v.ConfigTracerReporter(o.Tracer)
	}
	return o
}

// +---------------------------------------------------------------------------+
// | Access methods                                                            |
// +---------------------------------------------------------------------------+

func (o *Config) ethernet() (addr string) {
	var (
		err  error
		list []net.Addr
		sa   = make([]string, 0)
	)

	// Use
	// internal ethernet.
	defer func() {
		if len(sa) == 0 {
			sa = []string{"0.0.0.0"}
		}
		addr = strings.Join(sa, ",")
	}()

	// Networking interfaces detect.
	if list, err = net.InterfaceAddrs(); err == nil {
		for _, address := range list {
			if v, ok := address.(*net.IPNet); ok {
				if v4 := v.IP.To4(); v4 != nil && !v.IP.IsLoopback() {
					sa = append(sa, v4.String())
				}
			}
		}
	}
	return
}

func (o *Config) init() *Config {
	// Process id.
	o.Pid = os.Getpid()

	// Time format.
	if o.TimeFormat == "" {
		o.TimeFormat = DefaultTimeFormat
	}

	// Service address based on networking detect.
	o.ServiceAddr = o.ethernet()

	// Service name.
	if o.ServiceName == "" {
		o.ServiceName = DefaultServiceName
	}

	// Service version.
	if o.ServiceVersion == "" {
		o.ServiceVersion = DefaultServiceVersion
	}

	// OpenTracing for trace id.
	if o.OpentracingTraceId == "" {
		o.OpentracingTraceId = DefaultOpentracingTraceId
	}

	// Opentracing for span id.
	if o.OpentracingSpanId == "" {
		o.OpentracingSpanId = DefaultOpentracingSpanId
	}

	// Package for span position.
	if o.OpentracingSpanPosition == "" {
		o.OpentracingSpanPosition = DefaultOpentracingSpanPosition
	}

	// Set
	// standard adapter and level and tracer.
	o.SetAdapter(o.Adapter.String())
	o.SetLevel(o.Level.String())
	o.SetTracer(o.Tracer.String())

	// Init
	// logging with adapter.
	switch o.Adapter {
	case base.AdapterElasticsearch:
		if o.ElasticAdapter == nil {
			o.ElasticAdapter = &ElasticAdapterConfig{}
		}
		o.ElasticAdapter.init()
	case base.AdapterFile:
		if o.FileAdapter == nil {
			o.FileAdapter = &FileAdapterConfig{}
		}
		o.FileAdapter.init()
	case base.AdapterKafka:
		if o.KafkaAdapter == nil {
			o.KafkaAdapter = &KafkaAdapterConfig{}
		}
		o.KafkaAdapter.init()
	case base.AdapterTerm:
		if o.TermAdapter == nil {
			o.TermAdapter = &TermAdapterConfig{}
		}
		o.TermAdapter.init()
	}

	// Init
	// tracing with tracer.
	switch o.Tracer {
	case base.TracerJaeger:
		if o.JaegerTracer == nil {
			o.JaegerTracer = &JaegerTracerConfig{}
		}
		o.JaegerTracer.init()
	case base.TracerZipkin:
		if o.ZipkinTracer == nil {
			o.ZipkinTracer = &ZipkinTracerConfig{}
		}
		o.ZipkinTracer.init()
	}

	return o
}

func (o *Config) scan() *Config {
	for _, s := range []string{
		"config/log.yaml", "config/log.yml",
		"../config/log.yaml", "../config/log.yml",
	} {
		if body, err := os.ReadFile(s); err == nil {
			if err = yaml.Unmarshal(body, o); err == nil {
				break
			}
		}
	}
	return o
}

func (o *Config) scanService() *Config {
	p := &struct {
		Port    int    `yaml:"port"`
		Name    string `yaml:"name"`
		Version string `yaml:"version"`
	}{}

	for _, s := range []string{
		"config/app.yaml", "config/app.yml",
		"../config/app.yaml", "../config/app.yml",
	} {
		if body, err := os.ReadFile(s); err == nil {
			if err = yaml.Unmarshal(body, p); err == nil {
				if p.Name != "" {
					o.ServiceName = p.Name
				}
				if p.Version != "" {
					o.ServiceVersion = p.Version
				}
				if p.Port > 0 {
					o.ServicePort = p.Port
				}
				break
			}
		}
	}
	return o
}
